
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: SidekarFilter.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar.spring;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * A servlet {@link javax.servlet.Filter} that wraps requests inside a call to
 * {@link SidekarEnvironment#performAction SidekarEnvironment.performAction()}
 * or {@link SidekarEnvironment#performTransaction SidekarEnvironment.performTransaction()}.
 * This will result in cursors being closed automatically and (optionally) executes the servlet within a transaction.
 *
 * <p>
 * By default, {@link SidekarEnvironment#performAction SidekarEnvironment.performAction()} is used; to enable transactions,
 * configure the {@link #TRANSACTION_ISOLATION_PARAMETER "transactionIsolation"} init parameter and optionally the
 * {@link #TRANSACTION_DURABILITY_PARAMETER "transactionDurability"} and {@link #NO_WAIT_PARAMETER "noWait"} init parameters.
 * </p>
 *
 * <p>
 * This class finds the associated {@link SidekarEnvironment} bean in Spring's root web application context by name using the
 * {@link #SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER "sidekarEnvironmentBeanName"} filter init parameter specified in web.xml
 * (the default bean name is {@link #DEFAULT_SIDEKAR_ENVIRONMENT_BEAN_NAME "sidekarEnvironment"}). This class looks up the
 * {@link SidekarEnvironment} on each request to avoid initialization order issues, as when using Spring's
 * {@link org.springframework.web.context.ContextLoaderListener}, the root application context will get initialized after this filter.
 * </p>
 */
public class SidekarFilter extends OncePerRequestFilter {

    /**
     * Filter init parameter that specifies the name of the {@link SidekarEnvironment} bean: <code>{@value}</code>.
     *
     * @see #DEFAULT_SIDEKAR_ENVIRONMENT_BEAN_NAME
     */
    public static final String SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER = "sidekarEnvironmentBeanName";

    /**
     * The default name of the {@link SidekarEnvironment} bean: <code>{@value}</code>.
     *
     * @see #SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER
     */
    public static final String DEFAULT_SIDEKAR_ENVIRONMENT_BEAN_NAME = "sidekarEnvironment";

    /**
     * Filter init parameter that specifies a transaction isolation level: <code>{@value}</code>.
     * If this init parameter is not supplied, no transactions will be created. Otherwise, a value of
     * {@code "READ_UNCOMMITTED"}, {@code "READ_COMMITTED"}, {@code "REPEATABLE_READ"}, or {@code "SERIALIZABLE"}
     * should be specified. Default value is {@code "REPEATABLE_READ"}.
     */
    public static final String TRANSACTION_ISOLATION_PARAMETER = "transactionIsolation";

    /**
     * Filter init parameter that specifies a transaction durability: <code>{@value}</code>.
     * This init parameter is ignored unless {@link #TRANSACTION_ISOLATION_PARAMETER "transactionIsolation"} is also set.
     * Otherwise, a value of {@code "COMMIT_SYNC"}, {@code "COMMIT_NO_SYNC"}, or {@code "COMMIT_WRITE_NO_SYNC"}
     * should be specified. Default value is {@code "COMMIT_SYNC"}.
     */
    public static final String TRANSACTION_DURABILITY_PARAMETER = "transactionDurability";

    /**
     * Boolean init parameter that specifies whether the transaction should be configured to
     * not wait if a lock request cannot be immediately granted (if operating transactionally): <code>{@value}</code>.
     * Default value is {@code "false"}.
     */
    public static final String NO_WAIT_PARAMETER = "noWait";

    // Filter init parameters
    private String sidekarEnvironmentBeanName = DEFAULT_SIDEKAR_ENVIRONMENT_BEAN_NAME;
    private String transactionIsolation = "REPEATABLE_READ";
    private String transactionDurability = "COMMIT_SYNC";
    private boolean noWait;

    private TransactionConfig transactionConfig;

    /**
     * Get the name of the {@link SidekarEnvironment} bean to find in the Spring root application context.
     *
     * @see #SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER
     */
    public String getSidekarEnvironmentBeanName() {
        return this.sidekarEnvironmentBeanName;
    }

    /**
     * Set the name of the {@link SidekarEnvironment} bean to find in the Spring root application context.
     * Default is {@link #DEFAULT_SIDEKAR_ENVIRONMENT_BEAN_NAME}.
     *
     * @see #SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER
     */
    public void setSidekarEnvironmentBeanName(String sidekarEnvironmentBeanName) {
        this.sidekarEnvironmentBeanName = sidekarEnvironmentBeanName;
    }

    /**
     * Get the transaction isolation level to use, if any.
     *
     * @return transaction isolation, or {@code null} to not create transactions
     * @see #TRANSACTION_ISOLATION_PARAMETER
     */
    public String getTransactionIsolation() {
        return this.transactionIsolation;
    }

    /**
     * Set the transaction isolation level to use, if any.
     *
     * @param transactionIsolation transaction isolation, one of {@code "READ_UNCOMMITTED"}, {@code "READ_COMMITTED"},
     *  {@code "REPEATABLE_READ"}, or {@code "SERIALIZABLE"}, or {@code null} to not create transactions
     * @see #TRANSACTION_ISOLATION_PARAMETER
     */
    public void setTransactionIsolation(String transactionIsolation) {
        this.transactionIsolation = transactionIsolation;
    }

    /**
     * Get the transaction durability to use (if using transactions).
     *
     * @return transaction durability
     * @see #TRANSACTION_DURABILITY_PARAMETER
     */
    public String getTransactionDurability() {
        return this.transactionDurability;
    }

    /**
     * Set the transaction durability to use (if using transactions).
     *
     * @param transactionDurability transaction durability, one of {@code "COMMIT_SYNC"}, {@code "COMMIT_NO_SYNC"},
     *  or {@code "COMMIT_WRITE_NO_SYNC"}
     * @see #TRANSACTION_DURABILITY_PARAMETER
     */
    public void setTransactionDurability(String transactionDurability) {
        this.transactionDurability = transactionDurability;
    }

    /**
     * Get whether transactions should be configured to not wait if a lock request cannot immediately be granted
     * (if using transactions).
     *
     * @return whether to not wait
     * @see #NO_WAIT_PARAMETER
     */
    public boolean getNoWait() {
        return this.noWait;
    }

    /**
     * Set whether transactions should be configured to not wait if a lock request cannot immediately be granted
     * (if using transactions).
     *
     * @param noWait whether to not wait
     * @see #NO_WAIT_PARAMETER
     */
    public void setNoWait(boolean noWait) {
        this.noWait = noWait;
    }

    @Override
    protected void initFilterBean() throws ServletException {
        this.transactionConfig = getTransactionConfig();
    }

    /**
     * Create the {@link TransactionConfig} to be used for transactions based on the configured init parameters.
     *
     * @return transaction configuration, or {@code null} to not use transactions
     */
    protected TransactionConfig getTransactionConfig() throws ServletException {

        // Are we doing transactions?
        if (this.transactionIsolation == null)
            return null;

        // Parse isolation level
        TransactionConfig config = new TransactionConfig();
        if (this.transactionIsolation.equals("READ_UNCOMMITTED"))
            config.setReadUncommitted(true);
        else if (this.transactionIsolation.equals("READ_COMMITTED"))
            config.setReadCommitted(true);
        else if (this.transactionIsolation.equals("SERIALIZABLE"))
            config.setSerializableIsolation(true);
        else if (!this.transactionIsolation.equals("REPEATABLE_READ")) {
            throw new ServletException("invalid value `" + this.transactionIsolation
              + "' for init parameter `" + TRANSACTION_ISOLATION_PARAMETER + "'");
        }

        // Parse durability setting
        if (this.transactionDurability.equals("COMMIT_SYNC"))
            config.setDurability(Durability.COMMIT_SYNC);
        else if (this.transactionDurability.equals("COMMIT_WRITE_NO_SYNC"))
            config.setDurability(Durability.COMMIT_WRITE_NO_SYNC);
        else if (this.transactionDurability.equals("COMMIT_NO_SYNC"))
            config.setDurability(Durability.COMMIT_NO_SYNC);
        else {
            throw new ServletException("invalid value `" + this.transactionDurability
              + "' for init parameter `" + TRANSACTION_DURABILITY_PARAMETER + "'");
        }

        // Set noWait setting
        config.setNoWait(this.noWait);

        // Done
        return config;
    }

    /**
     * Look up the {@link SidekarEnvironment} that this filter should use.
     *
     * <p>
     * The implementation in {@link SidekarFilter} finds a bean with the name configured by the
     * {@link #SIDEKAR_ENVIRONMENT_BEAN_NAME_PARAMETER} ini parameter in the Spring root application context.
     *
     * @return the SidekarEnvironment to use
     * @see #getSidekarEnvironmentBeanName
     */
    protected SidekarEnvironment lookupSidekarEnvironment() {
        WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
        return wac.getBean(getSidekarEnvironmentBeanName(), SidekarEnvironment.class);
    }

    /**
     * Executes the servlet within a call to {@link SidekarEnvironment#performAction SidekarEnvironment.performAction()}
     * or {@link SidekarEnvironment#performTransaction SidekarEnvironment.performTransaction()}.
     */
    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
      final FilterChain filterChain) throws ServletException, IOException {

        // Create action
        VoidDatabaseAction action = new VoidDatabaseAction() {
            public void executeVoid() {
                try {
                    filterChain.doFilter(request, response);
                } catch (IOException e) {
                    throw new WrappedException(e);
                } catch (ServletException e) {
                    throw new WrappedException(e);
                }
            }
        };

        // Lookup environment
        SidekarEnvironment env = lookupSidekarEnvironment();

        // Perform (trans)action and unwrap any wrapped exception
        try {
            if (this.transactionConfig != null)
                env.performTransaction(this.transactionConfig, action);
            else
                env.performAction(action);
        } catch (WrappedException e) {
            e.rethrow();
        }
    }

    // This class allows us to transparently unwrap checked exceptions
    static final class WrappedException extends RuntimeException {

        private final Exception ex;

        public WrappedException(Exception ex) {
            super(ex);
            this.ex = ex;
        }

        public void rethrow() throws IOException, ServletException {
            if (this.ex instanceof IOException)
                throw (IOException)this.ex;
            if (this.ex instanceof ServletException)
                throw (ServletException)this.ex;
            throw this;
        }
    }
}

